Skip to content

Predbat-led EV charger control: shared mixin, and on automatically for GE, Ohme and Zappi (folds #5284 + #5311) - #5448

Open
chalfontchubby wants to merge 31 commits into
mainfrom
feat/charger-control
Open

chalfontchubby wants to merge 31 commits into
mainfrom
feat/charger-control

Conversation

@chalfontchubby

Copy link
Copy Markdown
Collaborator

Posted by Claude on behalf of @chalfontchubby.

Folds the former stack #5284 (shared charger-control mixin) and #5311 (control on automatically, Octopus Intelligent, guest charging) into one PR for a single final review, as Trefor suggested on #5284. It supersedes both, which will be closed once this is open. All their review threads were already answered and resolved; their commit history is kept as it was, so earlier review comments still map to commits.

Rebased onto main (8 Oct). One commit dropped: the gateway commit that moved its car plan parsing onto the shared parser, which main now has from #5120. Ohme's New Year tests from #5120 are ported to the shared control API. A local /code-review high before pushing found two small bugs in the existing code, fixed in the last commit: a dispatch time with no UTC offset raised a TypeError when compared with the clock (now read as UTC), and an is_charger of unknown/unavailable was read as "not the charger" instead of "cannot tell".

Known and not changed here (from the same review): a multi-car setup shares one Octopus-discovery timer, so a second car's discovery timeout can be masked by a first car that is already wired; the Ohme charger is keyed by serial or the literal "ohme", which would orphan held state if the serial arrives late; the guest switch republishes on the next poll rather than immediately.

Part 1: shared mixin (was #5284)

Summary

Predbat-led EV charger control (start/stop the charger from the car charging plan) was implemented three times, once each in gecloud.py (GivEnergy EV charger), myenergi.py (Zappi) and ohme.py, with a fourth window parser in gateway.py. This PR moves the shared loop into one mixin, CarChargerControl (car_charger_control.py), before we turn control on automatically and add more chargers (Hypervolt, Wallbox).

The mixin owns:

  • reading each car's plan from binary_sensor.predbat_car_charging_slot[_N]
  • on inside a window, off outside one, sending only on a change
  • re-applying when the charger has been changed elsewhere (drift)
  • releasing on read only mode or when the control switch is turned off, and resuming afterwards
  • persisting the control switch across restarts

Each component keeps only what is specific to its charger: which chargers exist and in which order, whether a car is connected, how drift is detected, and the on/off/release commands. Log wording for those commands is unchanged.

One commit per step; each passes the quick suite on its own:

  1. Add the mixin, with its own tests
  2. GivEnergy EVC onto it
  3. Zappi onto it
  4. Ohme onto it
  5. Gateway: shared window parser only
  6. Review fixes: release a charger whose car has gone, and retry refused GivEnergy commands
  7. Later review fixes: one charger refusing no longer blocks the others, and a released Zappi forgets its saved mode

Behaviour changes

Intended to be a refactor, with these exceptions:

  • New Year fix (Ohme, gateway). Their inline parsers missed this case: read just after midnight on 1 January, a window that started on 31 December parsed as the following December, so a charge running across midnight was paused/stopped. Both now use the shared parse_car_plan_windows(). The gateway copy also moved any window starting more than 23 hours ago a year forward; a finished window now stays finished.
  • Chargers with no car to follow. If there are more chargers than num_cars (briefly true while auto-config's num_cars reaches the base), an extra Zappi is now left alone instead of Stopped, matching what GivEnergy EVC already did. A charger Predbat was already holding when its car drops out of num_cars is released back to its own mode rather than left in the state Predbat last set.
  • Ohme release retry. A release Ohme refuses is now retried on the next control cycle rather than attempted once, so one failed call can't strand a paused car. Ohme's control tick is now wrapped like myenergi's: a refused command logs a warning instead of failing the whole run, which would otherwise repeat every minute while read only is on.
  • GE EVC refused commands retried. async_send_evc_command() reports a refusal by returning None, so a refused start, stop or release used to be recorded as sent and never retried; a refused release could strand a stopped charger. GE charger control now raises EVCCommandFailed on a refusal, so nothing is recorded and the command is tried again next cycle. GE's run loop catches it as a warning, as Ohme's and myenergi's do.
  • GE EVC read only fallback. When base.set_read_only is unset, the EVC loop now falls back to the set_read_only arg like the other chargers, not the switch entity. Predbat sets the attribute at startup (predbat.py), so this only affects test doubles. GECloudDirect.read_only_now() is kept for the inverter code that uses it.
  • Each charger handled on its own. Releasing and driving used to run as one loop, so the first charger to refuse a command stopped every charger after it, every cycle - with two Zappis held Stopped and read only on, a refusing Zappi 1 left Zappi 2 Stopped for good. Each charger is now handled on its own; the first failure is still raised afterwards for the run loop to log, and only the charger that refused is retried.
  • Zappi saved mode. A Zappi released because its car dropped out of num_cars kept its saved mode, so taking it back later restored that stale mode on the next release. The saved mode is now dropped once that Zappi's release goes through.
  • Some generic log lines (release, resume, drift) now share one wording.

Gateway control is not moved onto the mixin here: that would add release on read only, which is a behaviour change. It belongs with the follow-up that turns charger control on automatically.

Testing

  • New tests/test_car_charger_control.py covers the mixin against a fake component.
  • Existing GE, myenergi, Ohme and gateway control tests: internal names updated, assertions unchanged. Exceptions:
    • The gateway year-boundary test pinned the old "older than 23 hours means next year" heuristic. It is replaced by the two real New Year cases on a fixed clock, plus a finished-window case.
    • Ohme's New Year test gains the after-midnight case.
    • Two new myenergi tests cover a failed release being retried with the saved mode intact, and a mode saved ahead of a refused command being forgotten on release. A cold review found both issues; the retry test fails without the fix.
    • A new Ohme test covers a refused release being retried without failing run(), and a new mixin test covers a release that fails part-way through several chargers. Both come from an adversarial review; the Ohme test fails without the fix.
  • Review fixes: a mixin test covers a held charger whose car drops out of num_cars being released, and GE control tests 11 and 12 cover a refused stop and a refused release being retried rather than recorded.
  • Later review fixes: a mixin test covers a refusing charger not blocking the others (and the existing part-way release test now expects only the refusing charger to be retried), and a myenergi test covers a released Zappi snapshotting its mode afresh when taken back.
  • tools/debug-journal.md's myenergi entry is updated for the renamed methods.
  • Rebased onto main on 5 Oct. Main's Ohme changes in feat(ohme): take the car plan from Ohme's own schedule when Predbat is not controlling the charger #5401 and fix(ohme): leave the Intelligent car slots to Octopus when the Intelligent device is the car #5405 re-decide control on every poll and hand the charger back when Octopus Intelligent takes over. That hand-back now goes through the mixin (charger_control_state / charger_control_release()), with the same retry until it succeeds, and feat(ohme): take the car plan from Ohme's own schedule when Predbat is not controlling the charger #5401's tests are updated for the new state names.
  • Every commit passes pre-commit and the full suite on its own.
  • Not changed here, but worth knowing: a hand-back that Ohme refuses in that new fix(ohme): leave the Intelligent car slots to Octopus when the Intelligent device is the car #5405 path raises out of the poll as it did on main, whereas the control tick catches the same refusal as a warning. Making the two match would be a small follow-up.

Part 2: control on automatically (was #5311)

What this does

Stage 2 of Predbat-led EV charger control. It follows the rule agreed on #5284: if Predbat is planning the car and the user has turned on the charger's automatic setup, Predbat can control the charger without being asked separately.

Until now, ge_cloud_evc_control and myenergi_zappi_control were off unless set. They now have three states (ohme_control stays opt-in, see decision 2):

Setting Unset false true
ge_cloud_evc_control on when ge_cloud_automatic_evc is on off on, and warns if a prerequisite is missing
ohme_control off (unchanged): Ohme's own schedule is the car plan (#5401) off on
myenergi_zappi_control on only if the user wrote myenergi_automatic or myenergi_automatic_zappi in apps.yaml off on

It also adds three things, each described below:

  • a per-car rule for Octopus Intelligent;
  • a guest charging switch;
  • a few fixes found while reviewing it.

Behaviour change on upgrade

Users who already have ge_cloud_automatic_evc on now get charger control, and so do Zappi users who wrote myenergi_automatic into apps.yaml. Ohme users see no change. For each of them, Predbat starts and stops the charger unless they set its control setting to false.

Users who already had *_control: true see no change, apart from the new Octopus rule and the guest switch. One part of the Octopus rule does change things for them: where Octopus drives the car itself, Predbat now leaves the charger alone unless switch.predbat_octopus_intelligent_charger_follows_car is on, where before it drove the charger to its plan.

The docs say this in car-charging.md, in the new section "Predbat starting and stopping your charger". This probably wants a line in the release notes.

Decisions, and the options rejected

  1. Zappi: on only when the user wrote the automatic setting (fa7c9f6). myenergi_automatic and myenergi_automatic_zappi both default on, so the GE/Ohme rule would switch every Zappi user over on upgrade. A Zappi that Predbat controls is Stopped between windows, so it stops diverting solar in Eco/Eco+. The options considered are in enable_control()'s docstring, for review and to tidy up later:
    • Rejected: on for every Zappi user.
    • Chosen: on only when the user wrote the setting.
    • Rejected: Zappi stays opt-in.
  2. Ohme stays opt-in (e631a2c, replacing 7a35d85). Since feat(ohme): take the car plan from Ohme's own schedule when Predbat is not controlling the charger #5401, an Ohme that Predbat does not control already has its own schedule taken as the car plan, so the default user gets a plan that matches what the charger does. Turning control on by default would override that schedule, which the user may have set up deliberately, with max charge, which also overrides the Ohme app's target.
  3. Octopus Intelligent is judged per car (067f637, d3663f6, 3f6422c). You agreed this on refactor(car): share Predbat-led EV charger control across Ohme, Zappi and GivEnergy #5284. Predbat reads is_charger from the car's own dispatch sensor in octopus_intelligent_slot. Both the Octopus and Kraken components publish it, so E.ON/EDF SmartFlex is covered, and a car can't be paired with another car's device. What Predbat does depends on which device Octopus drives:
    • Octopus drives the charger: Predbat leaves the charger alone, whatever switch.predbat_octopus_intelligent_charging says. That switch only changes Predbat's own planning; Octopus keeps switching the charger either way, so driving it would fight Octopus.
    • Octopus drives the car: by default Predbat leaves the charger alone; a charger it was holding is released as usual, since nobody else drives it (5fa79fb, 0ab856d). A new expert-mode switch, switch.predbat_octopus_intelligent_charger_follows_car (default off), has Predbat drive the charger instead: on during dispatches and off otherwise, so the car cannot charge on its own timers. It follows the plan and also the dispatch times on the sensor, judged against the clock, so it picks up a new dispatch without waiting for the 5-minute plan and stops the charger when the dispatch ends. With octopus_intelligent_charging off, Predbat plans the car itself and the charger follows that plan, whatever the new switch says. The Ohme follows the same rules (bbbfb3f), so with the switch on ohme_control: true can run where fix(ohme): leave the Intelligent car slots to Octopus when the Intelligent device is the car #5405 would otherwise stand it down.
    • Can't tell (for example the Octopus Energy HA integration's sensor): Predbat leaves the charger alone unless *_control: true, and logs a warning naming that setting (6cec384). A charger Predbat was holding is handed back as usual, so one it had stopped is not stranded. Released users are unaffected, because before this PR control only ran with true.
    • Still discovering (the Octopus or Kraken component has not wired its devices yet): the charger is left as it is, and still held, until discovery finishes. After 15 minutes it is treated as can't tell, with a warning.
  4. Handing a charger to Octopus never starts a charge (d3663f6). A normal release undoes a stop, so that read only mode never leaves a car unable to charge. Octopus deciding when to charge is different, so the hand-over does this instead:
    • GE: sends nothing.
    • Ohme: turns max charge off, then resumes into its smart schedule.
    • Zappi: goes back to its own mode, with Eco+ in place of Fast.
  5. Guest charging switch (b6d30e4, 8032b55, ec86180). While Predbat drives a charger it holds it off outside the plan, so a visitor's car can't charge. The switch lets them:
    • The entities: switch.predbat_gecloud_guest_charging, switch.predbat_myenergi_guest_charging and switch.predbat_ohme_guest_charging.
    • What it does: releases the charger, as read only mode does. How the guest charges (boost, solar only) is chosen on the charger itself; Predbat never picks a mode.
    • When it ends: when a car plugged in after the switch went on is unplugged, using the plug state of all three chargers. Unplugging the owner's car to make way for the guest doesn't end it. It also ends after 12 hours.
    • Restarts: it isn't remembered across a restart.

Fixes found in review before opening

  • Octopus discovery: with octopus_automatic off, Predbat waited for Octopus discovery that never came, so control never turned on.
  • Setting values: a quoted "false" or a 0 read as on, because removing the defaults also skipped get_arg()'s boolean conversion. parse_control_setting() now normalises them.
  • Dispatch end: following the dispatch sensor's on/off state ran the charger up to about 3 minutes past the end of a dispatch.
  • GE status blip: an offline or unavailable status on a GE charger ended guest charging.

Found by a further /code-review after opening:

Rebased onto main (5 Oct)

The stack is rebased onto main, which meanwhile changed the Ohme component in #5401 and #5405. How the two fit:

Found by review after the rebase:

  • Can't tell stranded a GE charger: it was let go without being started, as if Octopus were taking over, so a charger Predbat had stopped stayed stopped with nobody in charge (5394c56).
  • Discovery waited for good: a Kraken component whose first run never succeeds left every unwired car's charger alone indefinitely. Now bounded at 15 minutes, and a held charger is left as it is while waiting rather than released, so a component restart does not start a charge (5394c56, 6405d2e, c8278fd).
  • A false warning on restart: the apps.yaml default's literal re: string, before its regex matches, was read as a dispatch sensor (5394c56).

Not in this PR (known and deferred)

  • Empty Zappi or Ohme: Predbat still sends commands to one with no car plugged in. Only GE skips an empty charger today. Using the plug state there is a separate change, because a car plugged into a Zappi left in Fast could charge for up to a minute before the next tick.
  • Guest energy: the guest's charging is still counted as the owner's car's.
  • GE size and limit: whether GE needs the same size/limit guard as the Ohme.
  • Ohme ohme_automatic_octopus_intelligent: has the same quoted-"false" problem, from before this PR.
  • No cars: with num_cars at 0 there is no plan to read, so control does nothing that cycle, including releasing a charger it holds. This comes from refactor(car): share Predbat-led EV charger control across Ohme, Zappi and GivEnergy #5284 and is unchanged.
  • Hardware: not tested on real chargers. In particular it is unverified that an Ohme resumed into its smart schedule doesn't start charging outside a dispatch.

Testing

  • New tests: for each decision above, in test_car_charger_control.py, test_ge_cloud.py, test_ohme.py and test_myenergi.py.
  • Checks: every commit passes pre-commit and the full suite on its own.
  • Reviews: two rounds before opening and four after the rebase, each a cold subagent review plus a local /code-review high. Their findings are the fixes listed above.

🤖 Generated with Claude Code

chalfontchubby and others added 28 commits October 8, 2026 18:51
Ohme, myenergi and GivEnergy EVC each carry their own copy of the same
control loop. CarChargerControl owns it once: read the per-car plan,
charge inside a window and stop outside one, re-apply on drift, and
release on read only or when the control switch is turned off, with
the switch persisted across restarts. Components supply only the
charger-specific send, release, connected and drift checks.

Nothing uses it yet; the components move onto it in following commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GivEnergy EVC control moves onto CarChargerControl. The component keeps
what is GivEnergy's own: which chargers exist and in what order, whether
a car is connected, and the start-charge / stop-charge commands.

Behaviour is unchanged: the switch keeps its storage key, chargers
without a car are still left alone, and release still only restarts a
charger Predbat had stopped. The EVC read only check now falls back to
the set_read_only arg, like the other chargers, rather than the switch
entity; the fallback only applies before Predbat sets the attribute at
startup. The test double's get_arg answers set_read_only from the same
flag as its switch, as Predbat does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zappi control moves onto CarChargerControl. The component keeps what is
myenergi's own: Fast inside a window and Stopped outside one, drift
detected from the polled mode, and release back to the saved mode or
Eco+.

One deliberate change: a Zappi with no car to follow (more Zappis than
num_cars, briefly true while auto-config's num_cars reaches the base) is
now left alone rather than Stopped, matching what GivEnergy EVC control
already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ohme control moves onto CarChargerControl. The component keeps what is
Ohme's own: max charge inside a window and pause outside one, drift
detected from the session mode, and release back to Ohme's smart
schedule with the user's target percent restored. Ohme has no control
switch, so read only stays the only release.

Its inline plan parser goes too, in favour of the shared one. That
fixes a New Year case the copy missed: read just after midnight on
1 January, a window that started on 31 December parsed as the following
December, so a charge running across midnight was paused.

A release Ohme refuses is now retried on the next control cycle rather
than attempted once, so a paused car is not stranded by one failed
call. The control tick is wrapped the way myenergi's already is: a
refused command logs a warning instead of failing the whole run, which
would otherwise repeat every minute while read only stays on.

The mixin now checks for a switch before touching storage, so a
component with no switch never needs a storage attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… retry refused GE commands

- A charger Predbat holds that falls beyond the cars it follows (num_cars
  dropped) was never commanded again, left charging or stopped with
  nobody in control. apply() now releases it.
- GivEnergy's async_send_evc_command() reports a refusal by returning
  None, so a refused release was taken as done and a stopped charger
  stranded - the failure the Ohme and myenergi retries already close.
  GE charger control now raises EVCCommandFailed on a refusal, so the
  command is not recorded and is tried again next cycle; GE's run loop
  catches it as a warning, as Ohme's and myenergi's already do. A
  refused start or stop is retried the same way rather than recorded as
  sent.
- Docstring for a nested test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eased Zappi forgets its mode

Found by a later review, in code from this PR:

- Releasing, releasing a charger whose car has gone, and driving from the plan ran as
  one loop, so the first charger to refuse a command stopped every charger after it -
  and each cycle retried in the same order, so they stayed blocked. With two Zappis
  held Stopped and read only on, a refusing Zappi 1 left Zappi 2 Stopped for good. Each
  charger is now handled on its own; the first failure is still raised after the rest
  have had their turn, for the run loop to log and retry. A charger is forgotten as
  soon as its own release goes through, so a retry re-sends only to the one that
  refused.
- A Zappi released on its own (its car gone when num_cars dropped) kept its saved
  mode, so taking it back later saved nothing new and the next release restored the
  stale mode. The saved mode is now dropped once that Zappi's release goes through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arger

Charger control now decides per car whether Octopus Intelligent is in
charge of the charger. Where Octopus delivers its dispatches by driving
the charger itself, Predbat leaves it alone - releasing it if it was
holding it - so the two never fight. Where Octopus drives the car, or
there is no Intelligent car, Predbat drives the charger from the car
slot sensor, which then carries the Octopus dispatches, so the car
cannot charge outside them on its own timers.

Octopus car N is the Nth wired Intelligent device, and the Octopus
component already records whether each is a charge point. When that
cannot be told - slots from outside the Octopus component, or before
it has discovered its devices - the charger is left alone, unless the
component's control setting is explicitly true.

Nothing turns control on by default yet; that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ge_cloud_evc_control no longer defaults to false. Left unset, control follows
ge_cloud_automatic_evc: setting that is the user asking Predbat to plan for the car,
and it is what maps each charger to its car. An explicit false keeps control off,
and only an explicit true warns when a prerequisite is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ohme_control no longer defaults to false. Left unset, control follows ohme_automatic,
which registers the car. Where the Intelligent slots come from the Ohme, Octopus
drives the charger and Predbat still leaves it alone - logged as Info for an unset
control, and as a warning only when ohme_control was explicitly set true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tomatic

myenergi_zappi_control no longer defaults to false. Left unset, control turns on
only when the user wrote myenergi_automatic or myenergi_automatic_zappi into
apps.yaml themselves. Both default on, so following their values alone - the rule
GE and Ohme use - would switch every Zappi user to Predbat control on upgrade, and a
Zappi held off between windows can no longer divert solar in Eco/Eco+. The options
considered are recorded in enable_control()'s docstring for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Intelligent

A new "Predbat starting and stopping your charger" section in car-charging.md sets
out when each charger's control turns on while its setting is unset, the upgrade
change for existing automatic users, and what happens on Octopus Intelligent
depending on whether Octopus drives the charger or the car. The per-setting entries
in apps-yaml.md and components.md now describe the unset default instead of false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er start a charge handing over

Found by review before opening the stage 2 PR:

- Read is_charger from the car's own wired octopus_intelligent_slot sensor instead of
  pairing car N with the Nth sorted Octopus device. Both the Octopus and Kraken
  components publish it there, so E.ON/EDF SmartFlex users are covered and a slot list
  in a different order can no longer pair a car with another car's device.
- Only wait for Octopus discovery when the Octopus component runs its automatic setup.
  With octopus_automatic off it never wires the slots, so control never turned on.
  "Cannot tell yet" also gets its own log line rather than claiming the car is on IOG.
- Handing a charger to Octopus no longer starts it. A normal release undoes a stop so
  a car is never stranded, but here Octopus decides when to charge: a GE charger
  Predbat stopped is left for Octopus, an Ohme has max charge turned off before it is
  resumed into its smart schedule, and a Zappi returns to its own mode, Eco+ in place
  of Fast.
- The control settings have no default, which also skipped get_arg()'s boolean
  conversion, so a quoted "false" or a 0 read as set. parse_control_setting()
  normalises them to None, True or False in each component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imit are set

Predbat drives an Ohme with max charge, which overrides the target set in the Ohme
app, so car_charging_battery_size and car_charging_limit are all that stop a charge.
Left at their defaults (100 kWh, 100%) the car would be charged to full. Unset
ohme_control now waits for both to be in apps.yaml and logs why; an explicit
ohme_control: true is still the user's call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pus drives follows its dispatch sensor

- switch.predbat_octopus_intelligent_charging only changes Predbat's own planning:
  Octopus goes on switching a charger that is its Intelligent device whatever the
  switch says. The device is now judged first, so turning the switch off no longer
  has Predbat fight Octopus for that charger. For a car Octopus drives, the switch
  still decides whether the charger follows the dispatches or Predbat's own plan.
- Where Octopus drives the car, the charger also runs while the car's dispatch sensor
  is on. The plan carries the dispatches but is only republished every 5 minutes and
  leaves out a dispatch Octopus has not given any energy yet, so the charger held the
  car off for the first minutes of a new dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While Predbat drives a charger it holds it off outside the planned windows, so a
visitor's car could not charge - and stage 2 turns control on by default. Each
component (GE, myenergi, Ohme) now publishes switch.predbat_<component>_guest_charging
beside its control. On, Predbat releases the charger as it does for read only mode, so
it runs on its own settings; how the guest charges is chosen on the charger itself.

It turns itself off when a connected car is unplugged, on chargers that can tell
(GE), and otherwise after GUEST_CHARGING_MAX_HOURS (12). It is deliberately not
persisted, so a restart puts Predbat back in charge. The guest's charging is still
counted as the car's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or Ohme

Both report whether a car is plugged in - the Zappi's plug status, the Ohme's
UNPLUGGED status - so guest charging now ends on unplug for every charger, not just
GE. A new charger_control_car_plugged() hook carries it, separate from
charger_control_connected(), so how Predbat drives an empty Zappi or Ohme is
unchanged. An unknown plug state reads as plugged in, leaving the 12 hour limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ends are judged by the clock

Found by review before opening the stage 2 PR:

- Guest charging ended on any unplug, including the owner's car being unplugged to make
  way for the guest. Only a car plugged in after the switch went on now ends it
  (empty, plugged in, empty again); a guest who plugged in first is left to the 12 hour
  limit or a manual off.
- A GE status blip (offline, unavailable, unknown) read as an unplug and ended guest
  charging. Only "available" and "idle" count as empty now, as the Zappi and Ohme
  already treat unknown states as plugged in.
- The charger followed the dispatch sensor's on/off state, which only refreshes every
  couple of minutes and so ran the charger past the end of a dispatch. The dispatch
  times on the sensor are now judged against the clock, in both the Octopus and the
  Kraken formats, falling back to the state for a sensor with no times.
- A toggle on the guest switch now flips it rather than turning it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rger

A dispatch sensor that does not say which device Octopus drives (the Octopus Energy
integration, for one) leaves the charger uncontrolled for good, and was only an Info
line. It is now a warning naming the component's own setting to set true. Waiting for
the Octopus component to discover its devices, which normally clears within a cycle
or two, stays Info. Each reason is logged once, and again only when it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g a charger (#5311 review)

Only the Octopus component was waited for. After a restart a Kraken (E.ON/EDF
SmartFlex) user's charger could be driven before Kraken had wired
octopus_intelligent_slot - stopping a GE charger mid-dispatch, which the hand-over on
the next cycle then left stopped. Kraken wires its devices in its first successful run,
from cache or fresh discovery, so until it has started the charger is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it, and stop waiting on discovery after 15 minutes (#5311 review)

- Cannot tell whether Octopus drives a charger: a charger Predbat had stopped
  was let go without being started, as if Octopus were taking it over. With
  nobody known to take it over, a GE charger was left stopped indefinitely.
  It is now released as usual; the non-starting hand-over is kept for a
  charger Octopus is known to drive.
- Waiting for Octopus or Kraken discovery is bounded at 15 minutes. A Kraken
  component whose first run never succeeds (a failed login) otherwise left
  every unwired car's charger uncontrolled for good. The log line now names
  both components.
- The apps.yaml default's literal "re:" string, before its regex matches, is
  no longer read as a dispatch sensor - which on every restart warned users
  with no Intelligent sensor to set a control override they do not need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at a discovery that never finishes as cannot tell (#5311 review)

Releasing during discovery started a charge Predbat had stopped whenever the
Octopus or Kraken component restarted, and driving the charger once the wait
ran out could fight Kraken if it was the one driving it. While discovery is
under way the charger is now left as it is and still held, so it is settled
either way once discovery finishes. After 15 minutes Predbat still cannot
tell, so it warns once and hands back a charger it holds, as for a dispatch
sensor that does not say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r is known (#5311 review)

The wait's start was only cleared when discovery was asked about with nothing
discovering, which stops happening once every car's driver is known. A later
component restart could then find the old start and count the wait as
already over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its own schedule stays the car plan otherwise

Main's #5401 now takes the car plan from Ohme's own schedule when Predbat is
not controlling the charger, so a default user already gets a plan that matches
what the charger will do. Turning control on by default would override that
schedule - one the user may have set up deliberately - with max charge, which
also overrides the Ohme app's target. ohme_control goes back to a false default;
GE and Zappi keep their automatic defaults. The size and limit guard for an
unset ohme_control goes, as there is no unset default any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alone when Octopus drives the car, unless asked to follow it

When the car itself is the Octopus Intelligent device, the car starts and stops
its own charge. Predbat now leaves the charger alone by default, handing over a
charger it held without starting a charge, as it does when Octopus drives the
charger. The new expert-mode switch, default off, has Predbat drive the charger
to the car's dispatches instead, so the car cannot also charge on its own
timers.

The Ohme follows the same switch: with it on, ohme_control can run while
Octopus drives the car, where #5405 otherwise stands it down. A change of the
switch is picked up on the next Ohme poll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llowing Predbat's own plan with Intelligent charging off, and log only real Ohme changes (#5311 review)

- Octopus driving the car means nobody drives the charger, so a charger
  Predbat had stopped is released as usual (GE started, Zappi back to its own
  mode including Fast) rather than handed over without starting, which left it
  stopped through every dispatch.
- With switch.predbat_octopus_intelligent_charging off Predbat plans the car
  itself, so it still drives the charger to that plan whatever the new switch
  says.
- An Ohme poll that sees only the new switch change logs that change, and only
  re-decides control while Octopus drives another device - not the Octopus
  tariff change it used to report.
- The log points at expert mode for the switch, and the class docstring says
  the charger is left alone by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ging off while Octopus drives the car (#5311 review)

The shared charger rule drives a charger for a car Octopus drives when
switch.predbat_octopus_intelligent_charging is off, as Predbat then plans the
car itself; the Ohme stood control down there regardless. It now follows the
same rule, and re-decides when either switch changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…harger value as cannot tell (local review)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chalfontchubby chalfontchubby added enhancement New feature or request refactor labels Oct 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Ohme’s live Intelligent handoff uses the unsafe release order, and persistence failures can be silently ignored.

2 open findings
What changed in this PR

Introduces shared Predbat-led EV charger control for GivEnergy, Ohme, and myenergi, including automatic activation, Intelligent dispatch handling, and guest charging.

Changes:

  • Adds the shared CarChargerControl mixin.
  • Migrates three charger integrations and adds extensive tests.
  • Updates configuration and user documentation.
File Description
apps/​predbat/​car_charger_control.py Adds shared charger-control logic.
apps/​predbat/​gecloud.py Migrates GivEnergy charger control.
apps/​predbat/​myenergi.py Migrates Zappi charger control.
apps/​predbat/​ohme.py Migrates Ohme control and Intelligent handling.
apps/​predbat/​components.py Adds tri-state control configuration.
apps/​predbat/​config.py Adds the follows-car switch.
apps/​predbat/​unit_test.py Registers shared-control tests.
apps/​predbat/​tests/​test_car_charger_control.py Tests the shared mixin.
apps/​predbat/​tests/​test_ge_cloud.py Expands GivEnergy control tests.
apps/​predbat/​tests/​test_myenergi.py Expands Zappi control tests.
apps/​predbat/​tests/​test_ohme.py Expands Ohme control tests.
docs/​apps-yaml.md Documents automatic-control settings.
docs/​car-charging.md Documents Intelligent and guest charging.
docs/​components.md Updates component configuration guidance.
tools/​debug-journal.md Updates myenergi diagnostic notes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/car_charger_control.py Outdated
Comment thread apps/predbat/ohme.py Outdated
chalfontchubby and others added 3 commits October 8, 2026 19:50
… when Storage refuses to save the control switch (#5448 review)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…charge Predbat started (#5448 review)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants